package com.atguigu.service.impl;

import com.atguigu.cache.ShopCache;
import com.atguigu.constant.RedisConst;
import com.atguigu.entity.ProductSalePropertyKey;
import com.atguigu.entity.SkuImage;
import com.atguigu.entity.SkuInfo;
import com.atguigu.exception.SleepUtils;
import com.atguigu.mapper.ProductSalePropertyKeyMapper;
import com.atguigu.mapper.SkuSalePropertyValueMapper;
import com.atguigu.service.SkuDetailService;
import com.atguigu.service.SkuImageService;
import com.atguigu.service.SkuInfoService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;

@Service
public class SkuDetailServiceImpl implements SkuDetailService {
    @Autowired
    private SkuInfoService skuInfoService;
    @Autowired
    private SkuImageService skuImageService;
    @Autowired
    private ProductSalePropertyKeyMapper salePropertyKeyMapper;
    @Autowired
    private SkuSalePropertyValueMapper skuSalePropertyValueMapper;
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private RBloomFilter<Long> skuBloomFilter;

    //@Cacheable(value = "xxx",key = "xxx")
    //@ShopCache(prefix = "#{#params}")
    @ShopCache(prefix = "skuInfo")
    @Override
    public SkuInfo getSkuInfo(Long skuId) {
        SkuInfo skuInfo=getSkuInfoFromDb(skuId);
        //SkuInfo skuInfo=getSkuInfoFromRedis(skuId);
        //SkuInfo skuInfo=getSkuInfoFromRedisWithThreadLocal(skuId);
        //SkuInfo skuInfo=getSkuInfoFromRedisson(skuId);
        return skuInfo;
    }
    //不能抵御随机缓存穿透
    private SkuInfo getSkuInfoFromRedisson1(Long skuId) {
        //a.从缓存中去拿数据 sku:24:info
        String cacheKey= RedisConst.SKUKEY_PREFIX+skuId+RedisConst.SKUKEY_SUFFIX;
        SkuInfo cacheSkuInfo=(SkuInfo)redisTemplate.opsForValue().get(cacheKey);
        if(cacheSkuInfo==null){
            String lockKey="lock-"+skuId;
            RLock lock = redissonClient.getLock(lockKey);
            lock.lock();
            try {
                //缓存双检查
                cacheSkuInfo=(SkuInfo)redisTemplate.opsForValue().get(cacheKey);
                if(cacheSkuInfo==null){
                    //b.从数据库中拿skuInfo的数据信息
                    SkuInfo skuInfoFromDb= getSkuInfoFromDb(skuId);
                    cacheSkuInfo=skuInfoFromDb;
                    //c.放到redis当中
                    redisTemplate.opsForValue().set(cacheKey,skuInfoFromDb,RedisConst.SKUKEY_TIMEOUT, TimeUnit.SECONDS);
                }
                return cacheSkuInfo;
            } finally {
                lock.unlock();
            }
        }
        //d.缓存中如果有值直接返回
        return cacheSkuInfo;
    }


    //能抵御随机缓存穿透
    private SkuInfo getSkuInfoFromRedisson(Long skuId) {
        //a.从缓存中去拿数据 sku:24:info
        String cacheKey= RedisConst.SKUKEY_PREFIX+skuId+RedisConst.SKUKEY_SUFFIX;
        SkuInfo cacheSkuInfo=(SkuInfo)redisTemplate.opsForValue().get(cacheKey);
        if(cacheSkuInfo==null){
            String lockKey="lock-"+skuId;
            RLock lock = redissonClient.getLock(lockKey);
            lock.lock();
            try {
                //缓存双检查
                cacheSkuInfo=(SkuInfo)redisTemplate.opsForValue().get(cacheKey);
                if(cacheSkuInfo==null){
                    //先问一下布隆过滤器是否有该id 如果有才查数据库
                    boolean flag = skuBloomFilter.contains(skuId);
                    SkuInfo skuInfoFromDb=null;
                    if(flag){
                        //b.从数据库中拿skuInfo的数据信息
                        skuInfoFromDb = getSkuInfoFromDb(skuId);
                    }
                    //不为空也需要放缓存 不管是否有都要放入redis中
                    redisTemplate.opsForValue().set(cacheKey,skuInfoFromDb,RedisConst.SKUKEY_TIMEOUT, TimeUnit.SECONDS);
                    return skuInfoFromDb;
                }
                return cacheSkuInfo;
            } finally {
                lock.unlock();
            }
        }
        //d.缓存中如果有值直接返回
        return cacheSkuInfo;
    }

    //1.从缓存中查询商品基本信息 能抵御随机缓存穿透
    public SkuInfo getSkuInfoFromRedis(Long skuId) {
        //a.从缓存中去拿数据 sku:24:info
        String cacheKey= RedisConst.SKUKEY_PREFIX+skuId+RedisConst.SKUKEY_SUFFIX;
        SkuInfo cacheSkuInfo=(SkuInfo)redisTemplate.opsForValue().get(cacheKey);
        if(cacheSkuInfo==null){
            //先问一下布隆过滤器是否有该id 如果有才查数据库
            boolean flag = skuBloomFilter.contains(skuId);
            SkuInfo skuInfoFromDb=null;
            if(flag){
                //b.从数据库中拿skuInfo的数据信息
                skuInfoFromDb = getSkuInfoFromDb(skuId);
            }
            //不为空也需要放缓存 不管是否有都要放入redis中
            redisTemplate.opsForValue().set(cacheKey,skuInfoFromDb,RedisConst.SKUKEY_TIMEOUT, TimeUnit.SECONDS);
            return skuInfoFromDb;
        }
        //d.缓存中如果有值直接返回
        return cacheSkuInfo;
    }


    //2.从缓存中查询商品基本信息 最为基础的功能
    public SkuInfo getSkuInfoFromRedis1(Long skuId) {
        //a.从缓存中去拿数据 sku:24:info
        String cacheKey= RedisConst.SKUKEY_PREFIX+skuId+RedisConst.SKUKEY_SUFFIX;
        SkuInfo cacheSkuInfo=(SkuInfo)redisTemplate.opsForValue().get(cacheKey);
        if(cacheSkuInfo==null){
            //b.从数据库中拿skuInfo的数据信息
            SkuInfo skuInfoFromDb = getSkuInfoFromDb(skuId);
            //不为空也需要放缓存
            redisTemplate.opsForValue().set(cacheKey,skuInfoFromDb,RedisConst.SKUKEY_TIMEOUT, TimeUnit.SECONDS);
            return skuInfoFromDb;
        }
        //d.缓存中如果有值直接返回
        return cacheSkuInfo;
    }



    //3.从缓存中查询商品基本信息 能解决缓存穿透问题 不能抵御随机缓存穿透
    public SkuInfo getSkuInfoFromRedis2(Long skuId) {
        //a.从缓存中去拿数据 sku:24:info
        String cacheKey= RedisConst.SKUKEY_PREFIX+skuId+RedisConst.SKUKEY_SUFFIX;
        SkuInfo cacheSkuInfo=(SkuInfo)redisTemplate.opsForValue().get(cacheKey);
        if(cacheSkuInfo==null){
            //b.从数据库中拿skuInfo的数据信息
            SkuInfo skuInfoFromDb = getSkuInfoFromDb(skuId);
            //判断skuInfoFromDb是否为空 如果为空
            if(skuInfoFromDb==null){
                //告诉你数据库确实没有数据 以后别查数据库了
                SkuInfo emptySkuInfo = new SkuInfo();
                //c.放到redis当中
                redisTemplate.opsForValue().set(cacheKey,emptySkuInfo,RedisConst.SKUKEY_TIMEOUT, TimeUnit.SECONDS);
                return  emptySkuInfo;
            }
            //不为空也需要放缓存
            redisTemplate.opsForValue().set(cacheKey,skuInfoFromDb,RedisConst.SKUKEY_TIMEOUT, TimeUnit.SECONDS);
            return skuInfoFromDb;
        }
        //d.缓存中如果有值直接返回
        return cacheSkuInfo;
    }


    //2.利用redis+lua+ThreadLocal实现商品基本信息查询
    public ThreadLocal<String> threadLocal=new ThreadLocal();
    public SkuInfo getSkuInfoFromRedisWithThreadLocal(Long skuId) {
        //a.从缓存中去拿数据 sku:24:info
        String cacheKey= RedisConst.SKUKEY_PREFIX+skuId+RedisConst.SKUKEY_SUFFIX;
        SkuInfo cacheSkuInfo=(SkuInfo)redisTemplate.opsForValue().get(cacheKey);
        if(cacheSkuInfo==null){
            //假如这上面还要做很多操作 取缓存 查逻辑等等 100行代码
            String token=threadLocal.get();
            boolean accquireLock=false;
            /**
             * 拼接lockKey  锁粒度问题
             *  锁越大 锁定的资源越多 等待时间越长 系统性能急剧下降
             *  锁越小 锁定的资源越少 等待时间越短 系统性能很强
             */
            String lockKey="lock-"+skuId;
            //线程刚开始进来的时候没有拿到锁
            if(token==null){
                token= UUID.randomUUID().toString();
                //利用redis的setnx命令
                accquireLock = redisTemplate.opsForValue().setIfAbsent(lockKey, token,3, TimeUnit.SECONDS);
            }else{
                accquireLock=true;
            }
            if(accquireLock){
                //b.从数据库中拿skuInfo的数据信息
                SkuInfo skuInfoFromDb = getSkuInfoFromDb(skuId);
                //c.放到redis当中
                redisTemplate.opsForValue().set(cacheKey,skuInfoFromDb,RedisConst.SKUKEY_TIMEOUT, TimeUnit.SECONDS);

                String redisToken =(String) redisTemplate.opsForValue().get(lockKey);
                String luaScript="if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
                redisScript.setScriptText(luaScript);
                redisScript.setResultType(Long.class);
                redisTemplate.execute(redisScript, Arrays.asList(lockKey),token);
                //防止内存泄露
                threadLocal.remove();

                return skuInfoFromDb;
            }else{
                //自旋 只去做一件事情 拿锁
                for(;;){
                    SleepUtils.sleepMillis(50);
                    boolean retryAccquireLock = redisTemplate.opsForValue().setIfAbsent(lockKey, token,3, TimeUnit.SECONDS);
                    if(retryAccquireLock){
                        //当前线程自旋成功 把拿到锁的token 放到threadLocal中
                        threadLocal.set(token);
                        break;
                    }
                }
                return getSkuInfoFromRedisWithThreadLocal(skuId);
            }
        }else{
            //防止内存泄露
            threadLocal.remove();
            //d.缓存中如果有值直接返回
            return cacheSkuInfo;
        }
    }



    private SkuInfo getSkuInfoFromDb(Long skuId) {
        //查询单个skuInfo信息
        SkuInfo skuInfo = skuInfoService.getById(skuId);
        if(skuInfo!=null){
            QueryWrapper<SkuImage> wrapper = new QueryWrapper<>();
            wrapper.eq("sku_id",skuId);
            List<SkuImage> skuImageList = skuImageService.list(wrapper);
            skuInfo.setSkuImageList(skuImageList);
        }
        return skuInfo;
    }

    //@ShopCache(prefix = "getSpuSaleProperteyAndSelected",enableBloom = false)
    @Override
    public List<ProductSalePropertyKey> getSpuSaleProperteyAndSelected(Long productId, Long skuId) {
        return salePropertyKeyMapper.getSpuSaleProperteyAndSelected(productId,skuId);
    }

    //@ShopCache(prefix = "getSalePropertyAndSkuIdMapping",enableBloom = false)
    @Override
    public Map<Object, Object> getSalePropertyAndSkuIdMapping(Long productId) {
        Map<Object, Object> salePropertyAndSkuId = new HashMap<>();
        List<Map> retMap=skuSalePropertyValueMapper.getSalePropertyAndSkuIdMapping(productId);
        for (Map map : retMap) {
            //key是销售属性的id对，value是skuId
            salePropertyAndSkuId.put(map.get("sale_property_value_id"),map.get("sku_id"));
        }
        return salePropertyAndSkuId;
    }
}
